package net.cyndeline.scalarlib.rldungeon.dgs.strategy

import net.cyndeline.scalarlib.rldungeon.dgs.graph.{DGSFunctionality, Room}
import net.cyndeline.scalarlib.rldungeon.dgs._
import scalax.collection.GraphEdge.UnDiEdge
import scalax.collection.GraphPredef.OuterEdge
import net.cyndeline.scalarlib.rldungeon.grammar.Strategy
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.PointlessAreaReductionStrategy
import scalax.collection.immutable.Graph
import com.escalatesoft.subcut.inject.{BindingModule, Injectable}
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.help._
import scala.reflect.runtime.universe._
import scala.reflect._
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.help.{BottleneckSetter, BottleneckSetterI}
import net.cyndeline.scalarlib.rlgraph.util.EdgeCopyFactory
import net.cyndeline.scalarlib.rldungeon.dgs.strategy.pointlessArea.help.StrategyGraphData

/**
 * This is the main object responsible for generating a level. It operates in three stages:
 *
 *  1. Topology: Rooms (vertices) and corridors (edges) are added to a map graph. This is the only stage in production
 *  where the map structure should be modified.
 *    - Pointless areas are computed and marked on the map once the topology is finished (areas the player doesn't
 *    need to visit to reach the goal).
 *  1. Pointless areas are dealt with by placing objects on the map that incentivise the player to explore the map.
 *  1. Object placement: The graph rooms are populated with objects (enemies, rewards, obstacles etc.).
 *
 * The final map is then returned.
 *
 * Note to users: Do not instantiate this class directly, refer to the factory object for creating new level strategies.
 *
 * @constructor Creates a new level strategy for generating levels.
 * @param topologyStrategy A sub-strategy for creating the topology of the level graph.
 * @param objectStrategy A sub-strategy for dealing with object placement in the level graph.
 * @param pointlessAreaReduction Strategy for making pointless areas meaningful.
 * @param edgeCopyFactory Factory that copies edges by replacing their vertices.
 * @param activatorResponderProcessor Factory that produces new vertex/edge pairs by adding responders and
 *                                    activators to them.
 * @param rewardProcessor Factory that produces new rooms by adding rewards to them.
 * @param responderAmount Algorithm that computes decides the maximum amount of responders that should be placed
 *                        on a map.
 */
class LevelStrategy[VType <: Room[VType], EType[X] <: UnDiEdge[X] with DGSFunctionality[X, EType] with OuterEdge[X, EType]]
  (topologyStrategy: Strategy[VType, EType],
   objectStrategy: Strategy[VType, EType],
   pointlessAreaReduction: PointlessAreaReductionStrategy,
   edgeCopyFactory: EdgeCopyFactory[VType, EType],
   activatorResponderProcessor: ActivatorResponderProcessor[VType, EType],
   rewardProcessor: RewardProcessor[VType, EType],
   responderAmount: ActivatorResponderAmountEstimator[VType, EType])
  (implicit val bindingModule: BindingModule, ct: ClassTag[VType], tt: TypeTag[VType])
   extends Strategy[VType, EType] with Injectable {

  /* Collapses cycles into super-nodes. */
  val superGraphFactory = injectOptional[SuperNodeFactoryInterface] getOrElse { new SuperNodeFactory() }

  /* Computes the shortest distance from the goal room to every other room. */
  val shortestDistanceAlgorithm = injectOptional[ShortestDistanceDesignatorI] getOrElse { new ShortestDistanceDesignator() }

  val bottleneckSetter = injectOptional[BottleneckSetterI] getOrElse { new BottleneckSetter() }

  /**
   * Produces a graph representation of a map.
   *
   * @param initialGraph The graph that the productions initially will be applied to. This graph may initially contain
   *                     whatever data the user wishes to use as starting point for the level, if any.
   * @return the finished map graph if one was possible to produce according to the strategy, otherwise None.
   */
  def apply(initialGraph: Graph[VType, EType]): Option[Graph[VType, EType]] = {

    // Create topology
    val topologyGraph = topologyStrategy.apply(initialGraph)

    if (!topologyGraph.isDefined)
      return None
    else if (topologyGraph.get.isEmpty)
      throw new Error("The produced topology graph was empty")

    // Check that the input graph is connected and has a start/goal
    val startAndGoal = findStartToGoal(topologyGraph.get)
    var start = startAndGoal._1
    var goal = startAndGoal._2

    // Compute shortest distances to goal
    val result: GraphModificationResult[VType, EType] = shortestDistanceAlgorithm.addShortestDistances(topologyGraph.get, goal, edgeCopyFactory)
    val graphWithShortestDistances = result.graph
    start = result.oldToNew(start)
    goal = result.oldToNew(goal)

    // Compute cycle-collapsed graph to use when removing pointless areas
    val collapsedGraph = superGraphFactory.collapseCycles(graphWithShortestDistances)

    // Mark which edges must be visited when exploring the map.
    val graphWithBottlenecks = bottleneckSetter.markBottlenecks(graphWithShortestDistances, collapsedGraph)

    // Remove pointless areas
    val graphData = StrategyGraphData(graphWithBottlenecks, collapsedGraph, start, goal)
    val graphWithoutPointlessAreas = pointlessAreaReduction.apply(graphData,
                                                                  activatorResponderProcessor,
                                                                  rewardProcessor,
                                                                  edgeCopyFactory,
                                                                  responderAmount)

    // Apply objects to topology, use the pre-pointless area graph if no areas were removed.
    val finishedTopologyGraph = graphWithoutPointlessAreas.getOrElse { graphWithBottlenecks }
    val finalGraph = objectStrategy.apply(finishedTopologyGraph)

    finalGraph
  }

  /**
   * Finds the start/goal rooms of a graph.
   * Throws exceptions if a graph is not connected and contains a single start and goal.
   * @return Start (_1) and goal (_2) of the level.
   */
  private def findStartToGoal(g: Graph[VType, EType]): (VType, VType) = {
    val start = g.nodes.filter(n => n.isStart)
    val goal = g.nodes.filter(n => n.isGoal)

    if (start.isEmpty)
      throw new Error("The produced graph topology did not contain a start room: " + g)
    else if (start.size > 1)
      throw new Error("The produced graph topology contained more than one start room: " + g)
    else if (goal.isEmpty)
      throw new Error("The produced graph topology did not contain a goal room: " + g)
    else if (goal.size > 1)
      throw new Error("The produced graph topology contained more than one goal room: " + g)
    else if (!g.isConnected)
      throw new Error("The produced graph topology is not connected: " + g)

    (start.head, goal.head)
  }


}
